import abc
import traceback
import weakref
from threading import Lock, Thread, current_thread
from time import sleep, time
from typing import Any, Callable, Iterable, Mapping


class RepeatingTimer(abc.ABC):
    def __init__(self,
        interval: float,
        func: Callable, *,
        args: Iterable[Any] = None,
        kwargs: Mapping[str, Any] = None,
    ):
        self._interval = interval

        self._func = func
        self._args = () if args is None else args
        self._kwargs = {} if kwargs is None else kwargs

        self._lock = Lock()
        self._thread = None

    @property
    def interval(self) -> float:
        return self._interval

    @property
    def alive(self) -> bool:
        with self._lock:
            return self._thread is not None

    def start(self):
        with self._lock:
            if self._thread:
                return
            self._thread = Thread(
                target=self._thread_func,
                args=(weakref.ref(self), self._interval),
                daemon=True)
            self._thread.start()

    def stop(self):
        with self._lock:
            if not self._thread:
                return
            thread = self._thread
            self._thread = None
        if thread is not current_thread():
            thread.join()

    def join(self):
        thread = self._thread
        if thread:
            thread.join()

    @staticmethod
    def _thread_func(self_ref, interval):
        scheduled_time = time() + interval
        while True:
            # check if alive
            self = self_ref()
            if self is None:
                break  # thread stops if the RepeatingTimer instance is destructed.
            with self._lock:
                if self._thread is not current_thread():
                    break  # thread stops if the _thread is not current thread.

            # tick
            try:
                self._func(*self._args, **self._kwargs)
            except Exception:
                traceback.print_exc()
                self.stop()

            # !! important !! release reference to LiveTime instance before sleeping
            del self

            # sleep for a certain time
            now_time = time()
            sleep_time = scheduled_time - now_time
            if sleep_time > 0:
                sleep(sleep_time)
                scheduled_time = scheduled_time + interval
            else:
                scheduled_time = now_time + interval
